Skip to content

[GR-54697] Implement debuginfo generation at image-runtime. #10522

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 24, 2025

Conversation

graalvmbot
Copy link
Collaborator

@graalvmbot graalvmbot commented Jan 21, 2025

Advanced Debugging of Native Images in GDB

Goals

  • Improve on the already available debug info generator
  • Implement run-time debug info generation
  • Notify GDB about new run-time debug info
  • Enhance the debugging experience in GDB

Debug info produced during a Native Image build will be referred to as AOT Debug Info, and debug info produced at image run-time as Run-time Debug Info.

Overview

On Linux, Native Image supports generating images with DWARF debug info.
This allows to use a native debugger (e.g. GDB) to debug native images.
This PR builds on the already available debug info generator and DWARF debug info.

With the existing implementaiton, debug info generation is part of the Native Image builder and only runs as part of the Native Image build process to produce AOT Debug Info.
For run-time compilations, there is no debug info generator available to produce run-time debug info.

To fix this, this PR reworks the debug info generator to be also usable at image run-time.
With a run-time debug info generator, we can then generate debug info whenever a new method is compiled and registered into the run-time code-cache.
This Run-time debug info also needs to be disposed once the run-time compilation in the run-time code-cache is no longer valid (e.g. after deoptimization).

Updates to Debug Info Generation

In general, debug info generation works in two steps:

  1. Produce and intermediate debug info representation -> Debug Entries
  2. Write the debug info to an object file -> Different backends possible, for example ELF/DWARF for Linux and PECOFF/CV for Windows

For this PR mainly focuses on 1 and writing ELF/DWARF debug info from 2 but not PECOFF/CV for Windows.

Debug Info Generator

Runs after all compilations are completed and makes use of:

  • Reachable types, fields, and methods from Point-to Analysis.
  • Compilation results from the compiler.

With this information, the debug info generator produces debug entries.
This PR changes this step as follows:

  • Parallelize producing debug entries

    • Number of threads can be set manually through -H:DebugInfoGenerationThreadCount=<threadCount> by default same as image builder
  • Extract a SharedDebugInfoProvider for use at image build-time and image run-time.

    • Independent of the Native Image Builder.

    • Build-time implementation -> needs to generate as much debug info as possible. This is all classes, other types, methods, fields, compilations, locations of static fields and class constant objects.

    • Run-time implementation -> only needs to process the run-time compilation. For types, it just requires the name of a type and its type signature. This reduces the time and memory needed for generating and storing run-time debug info object files.

Run-time Debug Info Generation

Can be enabled via -H:+RuntimeDebugInfo.
This adds the debug info generator into a Native Image (Only if run-time compilation support is enabled).
It only makes sense to use if the Native Image was built with debug info (-g).

Therefore, a Native Image build with run-time debug info support usually at least take the following arguments:

native-image -g -O0 -H:+RuntimeDebugInfo ...

Run-time debug info generation then works as follows:

  • What happens at image build-time:

    • Register a code observer that observers changes in the run-time code-cache.
    • Debug Info Generator and Object File Writer are added to the image.
  • What happens at image run-time:

    1. Compilation is added to the run-time code cache.
    2. The code observer is notified.
    3. Run-time debug info generator generates debug info for the run-time compilation.
    4. The Run-time Debug Info is written to an in-memory object file.
    5. Notify GDB about the newly available in-memory object file.

Deoptimization

If a run-time compilation is invalidated the run-time code cache (for example, due to deoptimization), we also have to clean up the in-memory object file containing the run-time debug info in reverse order:

  1. Notify GDB that the in-memory object file is no longer valid.
  2. Free all memory that was reserved for generating run-time debug info.

Tracking Run-time Debug Info

For each run-time compilation, we create a Handle that saves the current state of the run-time debug info (initialized, active, released) and a reference to the in-memory object file.
If a code-cache change is observed, the handle gets updated.

Possible state for a handle are:

  • Initialized.
  • Activated: the compilation is added to the run-time code-cache, debug info for the compilation is produced.
  • Released: the compilation is invalidated in the run-time code-cache.

Interfacing with GDB

Once run-time debug info is generated, the debugger needs to be notified.
GDB introduces the GDB JIT Compilation Interface that allows us to notify GDB about run-time debug info.

typedef enum
{
  JIT_NOACTION = 0,
  JIT_REGISTER_FN,
  JIT_UNREGISTER_FN
} jit_actions_t;

struct jit_code_entry
{
  struct jit_code_entry *next_entry;
  struct jit_code_entry *prev_entry;
  const char *symfile_addr;
  uint64_t symfile_size;
};

struct jit_descriptor
{
  uint32_t version;
  /* This type should be jit_actions_t, but we use uint32_t
     to be explicit about the bitwidth.  */
  uint32_t action_flag;
  struct jit_code_entry *relevant_entry;
  struct jit_code_entry *first_entry;
};

/* GDB puts a breakpoint in this function.  */
void __attribute__((noinline)) __jit_debug_register_code() { };

/* Make sure to specify the version statically, because the
   debugger may check the version before we can set it.  */
struct jit_descriptor __jit_debug_descriptor = { 1, 0, 0, 0 };

To make use of the JIT compilation interface, we just need to create a jit_code_entry add it to the __jit_debug_descriptor set the correct action_flag and call __jit_debug_register_code.

Implementation Details

The implementation for Native Image is written in SystemJava in GdbJitInterface.
It contains two functions, one for adding a run-time debug info object file through a jit_code_entry:

public static void registerJITCode(CCharPointer addr, @CUnsigned long size, JITCodeEntry entry) {
        /* Create new jit_code_entry */
        entry.setSymfileAddr(addr);
        entry.setSymfileSize(size);

        /* Insert entry at head of the list. */
        JITCodeEntry nextEntry = jitDebugDescriptor.get().getFirstEntry();
        entry.setPrevEntry(Word.nullPointer());
        entry.setNextEntry(nextEntry);

        if (nextEntry.isNonNull()) {
            nextEntry.setPrevEntry(entry);
        }

        /* Notify GDB. */
        jitDebugDescriptor.get().setActionFlag(JITActions.JIT_REGISTER.getCValue());
        jitDebugDescriptor.get().setFirstEntry(entry);
        jitDebugDescriptor.get().setRelevantEntry(entry);
        jitDebugRegisterCode(CurrentIsolate.getCurrentThread());
    }

and one for removing it again:

public static void unregisterJITCode(JITCodeEntry entry) {
        JITCodeEntry prevEntry = entry.getPrevEntry();
        JITCodeEntry nextEntry = entry.getNextEntry();

        /* Fix prev and next in list */
        if (nextEntry.isNonNull()) {
            nextEntry.setPrevEntry(prevEntry);
        }

        if (prevEntry.isNonNull()) {
            prevEntry.setNextEntry(nextEntry);
        } else {
            assert (jitDebugDescriptor.get().getFirstEntry().equal(entry));
            jitDebugDescriptor.get().setFirstEntry(nextEntry);
        }

        /* Notify GDB. */
        jitDebugDescriptor.get().setActionFlag(JITActions.JIT_UNREGISTER.getCValue());
        jitDebugDescriptor.get().setRelevantEntry(entry);
        jitDebugRegisterCode(CurrentIsolate.getCurrentThread());
    }

Those methods are called after the code observer is notified on a run-time code cache change:

  • On initialization -> create jit_code_entry
  • On activation -> fill jit_code_entry and call registerJITCode
  • On release -> call unregisterJITCode and free jit_code_entry

Re-using AOT Debug Info

The initial idea for re-using AOT debug info was to make use of DWARF Type Units introduced in GR-56599.

DWARF Type Units mainly simplifies type references within DWARF debug information.

DWARF Type Units (TU)s

In contrast to regular type references that are relative to the start of a CU or the .debug_info section, TUs are referenced by a so-called type signature.
A type unit contains all static information about a type, for example:

  • Method declaration
  • Field declaration
  • Size
  • Classloader (modelled as DWARF namespaces)
  • ...

It should not contain any relocatable information, this should be placed in a CU.
Each TU that represents a Java class has a corresponding CU (both in the .debug_info section).
The corresponding CU then holds debug information for static field locations and method compilation (including all debug information for inlined methods).
The type signature of a TU is stored in the TU header as follows (excerpt from objdump for java.lang.Object):

Compilation Unit @ offset 0x455b2e:
 Length: 0x1cf (32-bit)
 Version: 5
 Unit Type: DW_UT_type (2)
 Abbrev Offset: 0
 Pointer Size: 8
 Signature: 0x675f9f96109e8e1
 Type Offset: 0x1b
 <0><455b46>: Abbrev Number: 5 (DW_TAG_type_unit)
 <455b47> DW_AT_language : 11 (Java)
 <455b48> DW_AT_use_UTF8 : 1
 <1><455b49>: Abbrev Number: 11 (DW_TAG_class_type)
 <455b4a> DW_AT_name : (indirect string, offset: 0x9b465): java.lang.Object
 <455b4e> DW_AT_byte_size : 24
...

The corresponding CU refers to the TU by the signature and adds debug information to this type:

<0><455d6d>: Abbrev Number: 4 (DW_TAG_compile_unit)
    <455d70>   DW_AT_name        : java/lang/Object.java
    <455d74>   DW_AT_comp_dir    : sources
    <455d78>   DW_AT_ranges      : 0x10c53
    <455d7c>   DW_AT_low_pc      : 0x1e8600
    <455d84>   DW_AT_stmt_list   : 0xee8a4
    <455d88>   DW_AT_loclists_base: 0x30599a (location list)
 <1><455d8c>: Abbrev Number: 12 (DW_TAG_class_type)
    <455d8d>   DW_AT_name        : (indirect string, offset: 0x9b465): java.lang.Object
    <455d91>   DW_AT_declaration : 1
    <455d92>   DW_AT_signature   : signature: 0x675f9f96109e8e1
<static field & compilations>

Type references in Native Image debug info therefore looks as follows and can be used from anywhere within the same object file:

<2><455d4e>: Abbrev Number: 38 (DW_TAG_inheritance)
    <455d4f>   DW_AT_type        : signature: 0x675f9f96109e8e1
    <455d57>   DW_AT_data_member_location: 0
    <455d58>   DW_AT_accessibility: 1   (public)

The plan was to make use of TUs to re-use type information from the AOT Debug Info in the Run-time Debug Info.
This requires a change in GDB to allow TU lookup across multiple object files.
As part of my work I implemented a patch to make this work within GDB dominikmascherbauer/binutils-gdb#1

However, this patch violates invariants about type ownership in GDB and will not be added as is to GDB.

Opaque Types

Instead, we still use TUs, but in combination with opaque types.
Opaque types are a concept of GDB (not defined in DWARF) and allow to reference type information by name across multiple object files.
An opaque type can only be a pointer to a DWARF struct, class, or union DIE, for example:

<1><1717>: Abbrev Number: 14 (DW_TAG_structure_type)
    <1718>   DW_AT_name        : java.lang.Object
    <171c>   DW_AT_declaration : 1
 <1><171d>: Abbrev Number: 16 (DW_TAG_pointer_type)
    <171e>   DW_AT_byte_size   : 8
    <171f>   DW_AT_type        : <0x1717>

Using opaque types allows us to re-use type information from the AOT debug info in run-time debug info.
This makes up most of the debug information needed for run-time compilations.

In the run-time debug info, we just need to add the debug information for the compilation and use opaque types instead of writing the whole type hierarchy into the run-time debug info again.

Enhanced Debugging in GDB

We provide a Python script gdb-debughelpers.py that makes use of the GDB Python API.
This improves the debugging experience of Native Images in the GDB command line.

Provides support for:

  • Pretty-printing: Give a more Java-like experience when printing value -> e.g. automatic pointer dereferencing and pretty-printing of structures like ArrayList and HashMap
  • Resolving Run-time type from the Dynamic Hub.
  • Auto-completion: Enhanced auto-completion based on the run-time type of values.
  • Resolving deoptimized frames in a stack trace

Usage Example

  • Pretty printing and type resolution in GDB
import java.util.HashMap;
import java.util.Map;

public class HashMapTest {
  public static void main(String[] args) {
    Map<String, Integer> map = new HashMap<>();
    map.put("One", 1);
    map.put("Ten", 10);
    map.put("Hundred", 100);

    System.out.println(map.get("One"));
  }
}

Compile Java file with all debug information, then build a Native Image with debug info.

javac -g HashMapTest.java
native-image -g -O0 HashMapTest
  • Debugging run-time compiled code

Run-time debug info is tested against some manually triggered run-time compilations in RuntimeCompileDebugInfoTest.

  • Debugging run-time compilations and deoptimizations in js code:

First build a jsvm image with run-time debug info support.

native-image --macro:jsvm-library -g -O0 -H:+RuntimeDebugInfo

Then use it to run js code, for example:

function add(a, b, test) {
    if (test) {
        a += b;
    }
    return a + b;
}

// trigger compilation add for ints and test = true
for (let i = 0; i < 1000 * 1000; i++) {
    add(i, i, true);
}

// deoptimize with failed assumption in compiled method
// then trigger compilation again
console.log("deopt1")
for (let i = 0; i < 1000 * 1000; i++) {
    add(i, i, false);
}

// deoptimize with different parameter types
console.log("deopt2");
add({f1: "test1", f2: 2}, {x: "x", y: {test: 42}}, false);

@oracle-contributor-agreement oracle-contributor-agreement bot added the OCA Verified All contributors have signed the Oracle Contributor Agreement. label Jan 21, 2025
@olpaw
Copy link
Member

olpaw commented Mar 4, 2025

Even if we don't get the GDB changes upstream in time this PR should be merged soon. The Debuginfo generation speedup due to parallelization plus the fix to make our DWARF5 work with GDB 16 or later justifies having this on master.

@graalvmbot graalvmbot force-pushed the doma/GR-54697 branch 3 times, most recently from 8866afc to 3e9f02a Compare April 29, 2025 09:33
@graalvmbot graalvmbot force-pushed the doma/GR-54697 branch 3 times, most recently from 040181d to ac8e19c Compare May 14, 2025 12:12
@graalvmbot graalvmbot force-pushed the doma/GR-54697 branch 2 times, most recently from f6cb0b8 to da7b49d Compare July 2, 2025 11:13
@olpaw
Copy link
Member

olpaw commented Jul 3, 2025

@adinn this PR will likely be merged mid or end of next week. If you have any questions or comments, this would be a good time. cc @dominikmascherbauer

olpaw
olpaw previously approved these changes Jul 8, 2025
@olpaw
Copy link
Member

olpaw commented Jul 8, 2025

Caveat: For GDB to be able to resolve type references from run-time debuginfo to AOT debuginfo, currently a modified version of GDB is required. https://github.com/dominikmascherbauer/binutils-gdb
We are working on getting the required changes upstream.

This is not required anymore as we now use GDB opaque types to refer to already at-build-time-defined types from the run-time debuginfo generator.

@graalvmbot graalvmbot force-pushed the doma/GR-54697 branch 2 times, most recently from dddcdb2 to 3234927 Compare July 15, 2025 12:40
@graalvmbot graalvmbot force-pushed the doma/GR-54697 branch 3 times, most recently from 20dd01d to c2ad2f0 Compare July 21, 2025 08:20
…g information generation

Implement the GDB JIT compilation interface
Split off a shared debug information generator for use at image build-time and run-time
Rework debug entries
Parallelize debug information generation
Add LocalVariableTable to SubstrateMethod
Add tests for run-time debug information generation
Refactor and rework gdb-debughelpers
Make BFDNameProvider usable at image run-time
Update Line section to DWARF5, fix bug when loading debug info in recent GDB versions (>= GDB 16)
Add support for lazy deoptimization for frame unwinder and frame filter
Implement opaque type resolution for runtime debug information in GDB
Updates to CV debug information to be compatible with reworked debug information generation
@graalvmbot graalvmbot merged commit 95dd661 into master Jul 24, 2025
13 checks passed
@graalvmbot graalvmbot deleted the doma/GR-54697 branch July 24, 2025 03:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
OCA Verified All contributors have signed the Oracle Contributor Agreement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants